package coap.messages;

import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.LinkedList;

public class Message {
	//Java does not support unsigned types, so for simplicity,
	//we use int for all and convert them during serialization
	private int version = 1; //2-bit
	private int type; //2-bit
	private int code; //8-bit
	private int messageID; //16-bit
	
	private LinkedList<Option> options = new LinkedList<Option>();
	private byte[] payLoad = null;
	
	public Message(int type, int code, int messageID, byte[] payLoad){
		this.type  = type;
		this.code = code;
		this.messageID = messageID;
		this.payLoad = payLoad;
	}
	
	public Message(ByteBuffer buffer){
		this.deserialize(buffer);
	}
	
	public int getType(){
		return this.type;
	}
	
	public int getCode(){
		return this.code;
	}
	
	public int getMessageID(){
		return this.messageID;
	}
	
	public byte[] getPayLoad(){
		return this.payLoad;
	}
	
	public void addOption(Option o){
		if(options.size()>=15)
			throw new RuntimeException("too many options");
		options.add(o);
	}
	
	public LinkedList<Option> getOptions(){
		return options;
	}

    public Option getOption(int optionNumber){
        for(Option o:options){
            if(o.getOptionNumber() == optionNumber)
                return o;
        }
        return null;
    }

    public void serialize(ByteBuffer buffer){
		byte firstByte = 0;
		firstByte|=(version<<6);
		firstByte|=(type<<4);
		firstByte|=options.size();
		buffer.put(firstByte);
		buffer.put((byte)this.code);
		buffer.putShort((short)this.messageID);
		Option []tmp = new Option[options.size()];
		int index=0;
		for(Option o:this.options){
			tmp[index++]=o;
		}
		Arrays.sort(tmp);
		
		for(int i=0; i<tmp.length; i++){
			int previousOptionNumber = 0;
			if(i>0)
				previousOptionNumber = tmp[i-1].getOptionNumber();
			if(tmp[i].getOptionNumber() - previousOptionNumber>14){
				Option fence = new Option(OptionNumber.Fenceposting, new byte[0]);
				fence.serialize(previousOptionNumber, buffer);
				previousOptionNumber = fence.getOptionNumber();
			}
			if(tmp[i].getOptionNumber()<previousOptionNumber)
				throw new RuntimeException("weird");
			tmp[i].serialize(previousOptionNumber, buffer);
				
		}
		buffer.put(this.payLoad);
	}
	
	private void deserialize(ByteBuffer buffer){
		byte firstByte =  buffer.get();
		this.version = (int)((firstByte>>6)&3);
		this.type = (int)((firstByte>>4)&3);
		int optionCount = (int)(firstByte&15);
		this.code = (int)(buffer.get());
		if(this.code<0)
			this.code+=256;
		this.messageID = (int)(buffer.getShort());
		if(this.messageID<0)
			this.messageID+=65536;
		int previousOptionNumber = 0;
		for(int i=0; i<optionCount; i++){
			Option o = new Option(previousOptionNumber, buffer);
			while(o.getOptionNumber()==OptionNumber.Fenceposting){
				previousOptionNumber = OptionNumber.Fenceposting;
				o = new Option(previousOptionNumber, buffer);
			}
			this.options.add(o);
			previousOptionNumber = o.getOptionNumber();
		}
		this.payLoad = new byte[buffer.remaining()];
		buffer.get(payLoad);
	}
	
	public boolean isRequest(){
		if(code>=1&&code<=31)
			return true;
		return false;
	}
	
	public boolean isResponse(){
		if(code>=64&&code<=191)
			return true;
		return false;
	}
	
	public boolean isEmpty(){
		if(code==0)
			return true;
		return false;
	}
	
	@Override
	public String toString(){
		StringBuilder builder = new StringBuilder();
        builder.append("\n===================\n");
        builder.append("ver=").append(this.version).append(" type=").append(MessageType.getString(type)).append("\n");
		builder.append("code=");
		if(isRequest())
			builder.append(MethodCodes.getString(code));
		else if(isResponse())
			builder.append(ResponseCodes.getString(code));
		else if(isEmpty())
			builder.append("empty");
		else
			builder.append("unknonw "+code);
		builder.append(" messageID=").append(this.messageID).append("\n");
		builder.append("opCount=").append(this.options.size()).append("\n");
		for(Option o:this.options){
			builder.append(o.toString()).append("\n");
		}
		builder.append("payLoad=").append(this.payLoad.length);
        builder.append("\n===================");
        return builder.toString();
	}
	
	public static void main(String []args){
		Message m = new Message(MessageType.Confirmable, ResponseCodes.Changed, 12340, new byte[1024]);
		m.addOption(new Option(OptionNumber.IfNoneMatch, new byte[14]));
		m.addOption(new Option(OptionNumber.ContentType, new byte[0]));
		m.addOption(new Option(OptionNumber.Etag, new byte[270]));
		System.out.println(m);
		byte []b1 = new byte[60000];
		ByteBuffer buffer1 = ByteBuffer.wrap(b1);
		m.serialize(buffer1);
		ByteBuffer buffer2 = ByteBuffer.wrap(b1, 0, buffer1.position());
		Message out = new Message(buffer2);
		System.out.println(out);
	}

    private int timeoutCount = 0;
    private long nextTimeoutTime = System.currentTimeMillis() + 500;
    private long timeoutInterval = 500;

    public boolean timeout(){
        timeoutCount ++;
        if(timeoutCount>5)
            return false;
        timeoutInterval *= 2;
        nextTimeoutTime += timeoutInterval;
        return true;
    }

    public long getNextTimeoutTime(){
        return this.nextTimeoutTime;
    }

    private InetAddress sender;
    private int senderPort;

    public void setSender(InetAddress sender, int senderPort){
        this.sender = sender;
        this.senderPort = senderPort;
    }

    public InetAddress getSender(){
        return sender;
    }

    public int getSenderPort(){
        return senderPort;
    }
}
